// ASN.1 JavaScript decoder
// Copyright (c) 2008-2014 Lapo Luchini <lapo@lapo.it>
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */
/*global oids */
import { Int10 } from './int10'

var ellipsis = '\u2026'
var reTimeS =
  /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/
var reTimeL =
  /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/
function stringCut(str, len) {
  if (str.length > len) {
    str = str.substring(0, len) + ellipsis
  }
  return str
}
var Stream = /** @class */ (function () {
  function Stream(enc, pos) {
    this.hexDigits = '0123456789ABCDEF'
    if (enc instanceof Stream) {
      this.enc = enc.enc
      this.pos = enc.pos
    } else {
      // enc should be an array or a binary string
      this.enc = enc
      this.pos = pos
    }
  }
  Stream.prototype.get = function (pos) {
    if (pos === undefined) {
      pos = this.pos++
    }
    if (pos >= this.enc.length) {
      throw new Error(
        'Requesting byte offset ' +
          pos +
          ' on a stream of length ' +
          this.enc.length
      )
    }
    return 'string' === typeof this.enc
      ? this.enc.charCodeAt(pos)
      : this.enc[pos]
  }
  Stream.prototype.hexByte = function (b) {
    return (
      this.hexDigits.charAt((b >> 4) & 0xf) + this.hexDigits.charAt(b & 0xf)
    )
  }
  Stream.prototype.hexDump = function (start, end, raw) {
    var s = ''
    for (var i = start; i < end; ++i) {
      s += this.hexByte(this.get(i))
      if (raw !== true) {
        switch (i & 0xf) {
          case 0x7:
            s += '  '
            break
          case 0xf:
            s += '\n'
            break
          default:
            s += ' '
        }
      }
    }
    return s
  }
  Stream.prototype.isASCII = function (start, end) {
    for (var i = start; i < end; ++i) {
      var c = this.get(i)
      if (c < 32 || c > 176) {
        return false
      }
    }
    return true
  }
  Stream.prototype.parseStringISO = function (start, end) {
    var s = ''
    for (var i = start; i < end; ++i) {
      s += String.fromCharCode(this.get(i))
    }
    return s
  }
  Stream.prototype.parseStringUTF = function (start, end) {
    var s = ''
    for (var i = start; i < end; ) {
      var c = this.get(i++)
      if (c < 128) {
        s += String.fromCharCode(c)
      } else if (c > 191 && c < 224) {
        s += String.fromCharCode(((c & 0x1f) << 6) | (this.get(i++) & 0x3f))
      } else {
        s += String.fromCharCode(
          ((c & 0x0f) << 12) |
            ((this.get(i++) & 0x3f) << 6) |
            (this.get(i++) & 0x3f)
        )
      }
    }
    return s
  }
  Stream.prototype.parseStringBMP = function (start, end) {
    var str = ''
    var hi
    var lo
    for (var i = start; i < end; ) {
      hi = this.get(i++)
      lo = this.get(i++)
      str += String.fromCharCode((hi << 8) | lo)
    }
    return str
  }
  Stream.prototype.parseTime = function (start, end, shortYear) {
    var s = this.parseStringISO(start, end)
    var m = (shortYear ? reTimeS : reTimeL).exec(s)
    if (!m) {
      return 'Unrecognized time: ' + s
    }
    if (shortYear) {
      // to avoid querying the timer, use the fixed range [1970, 2069]
      // it will conform with ITU X.400 [-10, +40] sliding window until 2030
      m[1] = +m[1]
      m[1] += +m[1] < 70 ? 2000 : 1900
    }
    s = m[1] + '-' + m[2] + '-' + m[3] + ' ' + m[4]
    if (m[5]) {
      s += ':' + m[5]
      if (m[6]) {
        s += ':' + m[6]
        if (m[7]) {
          s += '.' + m[7]
        }
      }
    }
    if (m[8]) {
      s += ' UTC'
      if (m[8] != 'Z') {
        s += m[8]
        if (m[9]) {
          s += ':' + m[9]
        }
      }
    }
    return s
  }
  Stream.prototype.parseInteger = function (start, end) {
    var v = this.get(start)
    var neg = v > 127
    var pad = neg ? 255 : 0
    var len
    var s = ''
    // skip unuseful bits (not allowed in DER)
    while (v == pad && ++start < end) {
      v = this.get(start)
    }
    len = end - start
    if (len === 0) {
      return neg ? -1 : 0
    }
    // show bit length of huge integers
    if (len > 4) {
      s = v
      len <<= 3
      while (((+s ^ pad) & 0x80) == 0) {
        s = +s << 1
        --len
      }
      s = '(' + len + ' bit)\n'
    }
    // decode the integer
    if (neg) {
      v = v - 256
    }
    var n = new Int10(v)
    for (var i = start + 1; i < end; ++i) {
      n.mulAdd(256, this.get(i))
    }
    return s + n.toString()
  }
  Stream.prototype.parseBitString = function (start, end, maxLength) {
    var unusedBit = this.get(start)
    var lenBit = ((end - start - 1) << 3) - unusedBit
    var intro = '(' + lenBit + ' bit)\n'
    var s = ''
    for (var i = start + 1; i < end; ++i) {
      var b = this.get(i)
      var skip = i == end - 1 ? unusedBit : 0
      for (var j = 7; j >= skip; --j) {
        s += (b >> j) & 1 ? '1' : '0'
      }
      if (s.length > maxLength) {
        return intro + stringCut(s, maxLength)
      }
    }
    return intro + s
  }
  Stream.prototype.parseOctetString = function (start, end, maxLength) {
    if (this.isASCII(start, end)) {
      return stringCut(this.parseStringISO(start, end), maxLength)
    }
    var len = end - start
    var s = '(' + len + ' byte)\n'
    maxLength /= 2 // we work in bytes
    if (len > maxLength) {
      end = start + maxLength
    }
    for (var i = start; i < end; ++i) {
      s += this.hexByte(this.get(i))
    }
    if (len > maxLength) {
      s += ellipsis
    }
    return s
  }
  Stream.prototype.parseOID = function (start, end, maxLength) {
    var s = ''
    var n = new Int10()
    var bits = 0
    for (var i = start; i < end; ++i) {
      var v = this.get(i)
      n.mulAdd(128, v & 0x7f)
      bits += 7
      if (!(v & 0x80)) {
        // finished
        if (s === '') {
          n = n.simplify()
          if (n instanceof Int10) {
            n.sub(80)
            s = '2.' + n.toString()
          } else {
            var m = n < 80 ? (n < 40 ? 0 : 1) : 2
            s = m + '.' + (n - m * 40)
          }
        } else {
          s += '.' + n.toString()
        }
        if (s.length > maxLength) {
          return stringCut(s, maxLength)
        }
        n = new Int10()
        bits = 0
      }
    }
    if (bits > 0) {
      s += '.incomplete'
    }
    return s
  }
  return Stream
})()
export { Stream }
var ASN1 = /** @class */ (function () {
  function ASN1(stream, header, length, tag, sub) {
    if (!(tag instanceof ASN1Tag)) {
      throw new Error('Invalid tag value.')
    }
    this.stream = stream
    this.header = header
    this.length = length
    this.tag = tag
    this.sub = sub
  }
  ASN1.prototype.typeName = function () {
    switch (this.tag.tagClass) {
      case 0: // universal
        switch (this.tag.tagNumber) {
          case 0x00:
            return 'EOC'
          case 0x01:
            return 'BOOLEAN'
          case 0x02:
            return 'INTEGER'
          case 0x03:
            return 'BIT_STRING'
          case 0x04:
            return 'OCTET_STRING'
          case 0x05:
            return 'NULL'
          case 0x06:
            return 'OBJECT_IDENTIFIER'
          case 0x07:
            return 'ObjectDescriptor'
          case 0x08:
            return 'EXTERNAL'
          case 0x09:
            return 'REAL'
          case 0x0a:
            return 'ENUMERATED'
          case 0x0b:
            return 'EMBEDDED_PDV'
          case 0x0c:
            return 'UTF8String'
          case 0x10:
            return 'SEQUENCE'
          case 0x11:
            return 'SET'
          case 0x12:
            return 'NumericString'
          case 0x13:
            return 'PrintableString' // ASCII subset
          case 0x14:
            return 'TeletexString' // aka T61String
          case 0x15:
            return 'VideotexString'
          case 0x16:
            return 'IA5String' // ASCII
          case 0x17:
            return 'UTCTime'
          case 0x18:
            return 'GeneralizedTime'
          case 0x19:
            return 'GraphicString'
          case 0x1a:
            return 'VisibleString' // ASCII subset
          case 0x1b:
            return 'GeneralString'
          case 0x1c:
            return 'UniversalString'
          case 0x1e:
            return 'BMPString'
        }
        return 'Universal_' + this.tag.tagNumber.toString()
      case 1:
        return 'Application_' + this.tag.tagNumber.toString()
      case 2:
        return '[' + this.tag.tagNumber.toString() + ']' // Context
      case 3:
        return 'Private_' + this.tag.tagNumber.toString()
    }
  }
  ASN1.prototype.content = function (maxLength) {
    if (this.tag === undefined) {
      return null
    }
    if (maxLength === undefined) {
      maxLength = Infinity
    }
    var content = this.posContent()
    var len = Math.abs(this.length)
    if (!this.tag.isUniversal()) {
      if (this.sub !== null) {
        return '(' + this.sub.length + ' elem)'
      }
      return this.stream.parseOctetString(content, content + len, maxLength)
    }
    switch (this.tag.tagNumber) {
      case 0x01: // BOOLEAN
        return this.stream.get(content) === 0 ? 'false' : 'true'
      case 0x02: // INTEGER
        return this.stream.parseInteger(content, content + len)
      case 0x03: // BIT_STRING
        return this.sub
          ? '(' + this.sub.length + ' elem)'
          : this.stream.parseBitString(content, content + len, maxLength)
      case 0x04: // OCTET_STRING
        return this.sub
          ? '(' + this.sub.length + ' elem)'
          : this.stream.parseOctetString(content, content + len, maxLength)
      // case 0x05: // NULL
      case 0x06: // OBJECT_IDENTIFIER
        return this.stream.parseOID(content, content + len, maxLength)
      // case 0x07: // ObjectDescriptor
      // case 0x08: // EXTERNAL
      // case 0x09: // REAL
      // case 0x0A: // ENUMERATED
      // case 0x0B: // EMBEDDED_PDV
      case 0x10: // SEQUENCE
      case 0x11: // SET
        if (this.sub !== null) {
          return '(' + this.sub.length + ' elem)'
        } else {
          return '(no elem)'
        }
      case 0x0c: // UTF8String
        return stringCut(
          this.stream.parseStringUTF(content, content + len),
          maxLength
        )
      case 0x12: // NumericString
      case 0x13: // PrintableString
      case 0x14: // TeletexString
      case 0x15: // VideotexString
      case 0x16: // IA5String
      // case 0x19: // GraphicString
      case 0x1a: // VisibleString
        // case 0x1B: // GeneralString
        // case 0x1C: // UniversalString
        return stringCut(
          this.stream.parseStringISO(content, content + len),
          maxLength
        )
      case 0x1e: // BMPString
        return stringCut(
          this.stream.parseStringBMP(content, content + len),
          maxLength
        )
      case 0x17: // UTCTime
      case 0x18: // GeneralizedTime
        return this.stream.parseTime(
          content,
          content + len,
          this.tag.tagNumber == 0x17
        )
    }
    return null
  }
  ASN1.prototype.toString = function () {
    return (
      this.typeName() +
      '@' +
      this.stream.pos +
      '[header:' +
      this.header +
      ',length:' +
      this.length +
      ',sub:' +
      (this.sub === null ? 'null' : this.sub.length) +
      ']'
    )
  }
  ASN1.prototype.toPrettyString = function (indent) {
    if (indent === undefined) {
      indent = ''
    }
    var s = indent + this.typeName() + ' @' + this.stream.pos
    if (this.length >= 0) {
      s += '+'
    }
    s += this.length
    if (this.tag.tagConstructed) {
      s += ' (constructed)'
    } else if (
      this.tag.isUniversal() &&
      (this.tag.tagNumber == 0x03 || this.tag.tagNumber == 0x04) &&
      this.sub !== null
    ) {
      s += ' (encapsulates)'
    }
    s += '\n'
    if (this.sub !== null) {
      indent += '  '
      for (var i = 0, max = this.sub.length; i < max; ++i) {
        s += this.sub[i].toPrettyString(indent)
      }
    }
    return s
  }
  ASN1.prototype.posStart = function () {
    return this.stream.pos
  }
  ASN1.prototype.posContent = function () {
    return this.stream.pos + this.header
  }
  ASN1.prototype.posEnd = function () {
    return this.stream.pos + this.header + Math.abs(this.length)
  }
  ASN1.prototype.toHexString = function () {
    return this.stream.hexDump(this.posStart(), this.posEnd(), true)
  }
  ASN1.decodeLength = function (stream) {
    var buf = stream.get()
    var len = buf & 0x7f
    if (len == buf) {
      return len
    }
    // no reason to use Int10, as it would be a huge buffer anyways
    if (len > 6) {
      throw new Error(
        'Length over 48 bits not supported at position ' + (stream.pos - 1)
      )
    }
    if (len === 0) {
      return null
    } // undefined
    buf = 0
    for (var i = 0; i < len; ++i) {
      buf = buf * 256 + stream.get()
    }
    return buf
  }
  /**
   * Retrieve the hexadecimal value (as a string) of the current ASN.1 element
   * @returns {string}
   * @public
   */
  ASN1.prototype.getHexStringValue = function () {
    var hexString = this.toHexString()
    var offset = this.header * 2
    var length = this.length * 2
    return hexString.substr(offset, length)
  }
  ASN1.decode = function (str) {
    var stream
    if (!(str instanceof Stream)) {
      stream = new Stream(str, 0)
    } else {
      stream = str
    }
    var streamStart = new Stream(stream)
    var tag = new ASN1Tag(stream)
    var len = ASN1.decodeLength(stream)
    var start = stream.pos
    var header = start - streamStart.pos
    var sub = null
    var getSub = function () {
      var ret = []
      if (len !== null) {
        // definite length
        var end = start + len
        while (stream.pos < end) {
          ret[ret.length] = ASN1.decode(stream)
        }
        if (stream.pos != end) {
          throw new Error(
            'Content size is not correct for container starting at offset ' +
              start
          )
        }
      } else {
        // undefined length
        try {
          for (;;) {
            var s = ASN1.decode(stream)
            if (s.tag.isEOC()) {
              break
            }
            ret[ret.length] = s
          }
          len = start - stream.pos // undefined lengths are represented as negative values
        } catch (e) {
          throw new Error(
            'Exception while decoding undefined length content: ' + e
          )
        }
      }
      return ret
    }
    if (tag.tagConstructed) {
      // must have valid content
      sub = getSub()
    } else if (
      tag.isUniversal() &&
      (tag.tagNumber == 0x03 || tag.tagNumber == 0x04)
    ) {
      // sometimes BitString and OctetString are used to encapsulate ASN.1
      try {
        if (tag.tagNumber == 0x03) {
          if (stream.get() != 0) {
            throw new Error('BIT STRINGs with unused bits cannot encapsulate.')
          }
        }
        sub = getSub()
        for (var i = 0; i < sub.length; ++i) {
          if (sub[i].tag.isEOC()) {
            throw new Error('EOC is not supposed to be actual content.')
          }
        }
      } catch (e) {
        // but silently ignore when they don't
        sub = null
      }
    }
    if (sub === null) {
      if (len === null) {
        throw new Error(
          "We can't skip over an invalid tag with undefined length at offset " +
            start
        )
      }
      stream.pos = start + Math.abs(len)
    }
    return new ASN1(streamStart, header, len, tag, sub)
  }
  return ASN1
})()
export { ASN1 }
var ASN1Tag = /** @class */ (function () {
  function ASN1Tag(stream) {
    var buf = stream.get()
    this.tagClass = buf >> 6
    this.tagConstructed = (buf & 0x20) !== 0
    this.tagNumber = buf & 0x1f
    if (this.tagNumber == 0x1f) {
      // long tag
      var n = new Int10()
      do {
        buf = stream.get()
        n.mulAdd(128, buf & 0x7f)
      } while (buf & 0x80)
      this.tagNumber = n.simplify()
    }
  }
  ASN1Tag.prototype.isUniversal = function () {
    return this.tagClass === 0x00
  }
  ASN1Tag.prototype.isEOC = function () {
    return this.tagClass === 0x00 && this.tagNumber === 0x00
  }
  return ASN1Tag
})()
export { ASN1Tag }
